package htmlsoup

import (
	"fmt"
	"net/url"
	"strings"

	"code.google.com/awwwe-browser/data"
)

func newSoupNode() *SoupNode {
	var v SoupNode
	//v.nodeType = nodeTypeDocument
	v.nodeCount = 0
	v.id = -1
	v.prefix = data.EmptyString
	v.name = data.EmptyString
	//v.statics.documentNameSpaces = nil
	v.nameSpaces = make(map[string]string)
	v.defaultNameSpace = data.EmptyString
	v.value = data.EmptyString
	v.attributes = buildNodeAttributeArray()
	v.attributesURLHandler = checkNoAttribute
	v.validParentNames = data.BuildSliceString(0, 5)
	v.inValidParentNames = data.BuildSliceString(0, 5)
	v.validChildrenNames = data.BuildSliceString(0, 5)
	v.forceClosedNames = data.BuildSliceString(0, 5)
	v.parent = nil
	v.children = BuildSliceSoupNode(0, 5)
	v.isBlock = false
	v.canBeBlock = false
	v.isHeadDecendent = false
	v.isBodyDecendent = false
	v.isValid = false
	v.isOutput = false
	v.useEndTag = false
	v.isClosed = false
	v.pauseBefore = false
	v.writeHandler = writeToBufferNil
	v.voiceHandler = writeToBufferNoVoice
	v.isVisible = false
	return &v
}

// Returns a root for starting a new html tree.
// The returned *SoupNode ( the root ) is also the current parent in the tree.
func NewDocumentNode(webPageURI string) *SoupNode {
	soupnode := newSoupNode()
	soupnode.nodeType = nodeTypeDocument
	soupnode.name = "document"
	soupnode.statics = newNodeStatics()
	soupnode.statics.rootSoupNode = soupnode
	soupnode.statics.location, _ = url.ParseRequestURI(webPageURI)
	soupnode.statics.baseURI, _ = url.ParseRequestURI(webPageURI)
	soupnode.parent = nil
	soupnode.isBlock = true
	soupnode.isValid = true
	soupnode.isOutput = true
	soupnode.isVisible = true
	soupnode.writeHandler = writeToBufferDocument
	return soupnode
}

// Adds a new doctype node to the tree.
// The returned *SoupNode is the current parent in the tree.
func AddNewDocTypeNode(parent *SoupNode, value []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeDocType
	soupnode.value = data.BytesToUTF8String(value)
	soupnode.statics = parent.statics
	soupnode.validParentNames.Append("document")
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = true
	soupnode.isVisible = true
	soupnode.parent = soupnode.findValidParent(parent)
	if parent != nil {
		soupnode.parent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.statics = soupnode.parent.statics
	soupnode.writeHandler = writeToBufferDocType
	newParent = soupnode.parent
	return
}

// Adds a new processor instruction node to the tree.
// The returned *SoupNode is the current parent in the tree.
func AddNewInstructionNode(parent *SoupNode, name, attributes []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeInstruction
	soupnode.name = strings.ToLower(data.BytesToUTF8String(name))
	soupnode.statics = parent.statics
	soupnode.parseAttributes(attributes)
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = true
	soupnode.isVisible = true
	soupnode.parent = soupnode.findValidParent(parent)
	if parent != nil {
		soupnode.parent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.statics = soupnode.parent.statics
	soupnode.writeHandler = writeToBufferInstruction
	newParent = soupnode.currentParent(parent)
	return
}

// Adds a new directive node to the tree.
// The returned *SoupNode is the current parent in the tree.
func AddNewDirectiveNode(parent *SoupNode, name, attributes []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeDirective
	soupnode.name = strings.ToLower(data.BytesToUTF8String(name))
	soupnode.statics = parent.statics
	soupnode.parseAttributes(attributes)
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = true
	soupnode.isVisible = true
	soupnode.parent = soupnode.findValidParent(parent)
	if parent != nil {
		soupnode.parent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.statics = soupnode.parent.statics
	soupnode.writeHandler = writeToBufferDirective
	newParent = soupnode.currentParent(parent)
	return
}

func AddNewConditionalCommentNode(parent *SoupNode, value []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeConditionalComment
	soupnode.statics = parent.statics
	soupnode.value = data.BytesToUTF8String(value)
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = false
	soupnode.isVisible = true
	soupnode.parent = soupnode.findValidParent(parent)
	if parent != nil {
		soupnode.parent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.statics = soupnode.parent.statics
	soupnode.writeHandler = writeToBufferConditionalComment
	newParent = soupnode.currentParent(parent)
	return
}

// Adds a new comment node to the tree.
// The returned *SoupNode is the current parent in the tree.
func AddNewCommentNode(parent *SoupNode, value []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeComment
	soupnode.statics = parent.statics
	soupnode.value = data.BytesToUTF8String(value)
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = false
	soupnode.isVisible = true
	soupnode.parent = soupnode.findValidParent(parent)
	if parent != nil {
		soupnode.parent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.statics = soupnode.parent.statics
	soupnode.writeHandler = writeToBufferComment
	newParent = soupnode.currentParent(parent)
	return
}

// Adds a new cdata node to the tree.
// The returned *SoupNode is the current parent in the tree.
func AddNewCdataNode(parent *SoupNode, value []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeCdata
	soupnode.statics = parent.statics
	soupnode.value = data.BytesToUTF8String(value)
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = false
	soupnode.isVisible = true
	soupnode.parent = soupnode.findValidParent(parent)
	if parent != nil {
		soupnode.parent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.statics = soupnode.parent.statics
	soupnode.writeHandler = writeToBufferCdata
	newParent = soupnode.currentParent(parent)
	return
}

// Adds a new html tag node to the tree.
// The returned *SoupNode is the current parent in the tree.
func OpenNewTagNode(parent *SoupNode, prefix, name, attributes []byte, isClosed bool) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeTag
	// ignore namespaces
	// soupnode.prefix = data.BytesToUTF8String(prefix)
	soupnode.name = strings.ToLower(data.BytesToUTF8String(name))
	soupnode.statics = parent.statics
	soupnode.isClosed = isClosed
	soupnode.isVisible = true
	// default settings for the tag
	isForcedToSpan := false
	defaultSettings, ok := htmlTagDefaultSettings[data.BytesToUTF8String(name)]
	if ok == false {
		defaultSettings, ok = htmlTagDefaultSettings["span"]
		soupnode.name = "span"
		isForcedToSpan = ok
		soupnode.parseAttributes(attributes)
	} else {
		soupnode.parseAttributes(attributes)
	}
	soupnode.isValid = defaultSettings.isValid
	if ok {
		// name is a valid tag name
		// soupnode.isClosed =	!defaultSettings.useEndTag
		soupnode.useEndTag = defaultSettings.useEndTag
		if soupnode.useEndTag == false {
			soupnode.isClosed = true
		}
		soupnode.attributesURLHandler = defaultSettings.attributesURLHandler
		soupnode.isOutput = defaultSettings.isOutput
		soupnode.isBlock = defaultSettings.isBlock
		soupnode.isClosed = defaultSettings.isClosed
		soupnode.canBeBlock = defaultSettings.canBeBlock
		soupnode.pauseBefore = defaultSettings.pauseBefore
		soupnode.validParentNames.AppendSlice(defaultSettings.validParents)
		soupnode.inValidParentNames.AppendSlice(defaultSettings.inValidParents)
		soupnode.validChildrenNames.AppendSlice(defaultSettings.validChildren)
		soupnode.forceClosedNames.AppendSlice(defaultSettings.forcedClosed)
		validParent := soupnode.findValidParent(parent)
		if validParent != nil {
			// add this tag to the tree
			soupnode.parent = validParent
			validParent.children.Append(soupnode)
			// set the document name spaces and id
			// and maybe some attributes
			soupnode.initializeDocumentNameSpaces()
			// some tags force other tags to close
			// example : <p> forces the last <p> to close
			length := soupnode.forceClosedNames.Len()
			for i := 0; i < length; i++ {
				child := findLastOpenChild(validParent,
					soupnode.prefix, soupnode.forceClosedNames.AtIndex(i),
					soupnode.id)
				if child != nil {
					child.close()
				}
			}
		} else {
			// not able to find a place to put this node
			soupnode.isValid = false
		}
		if soupnode.isValid && ok {
			if soupnode.statics.haveDefaultNameSpace == false {
				soupnode.statics.defaultNameSpace = soupnode.prefix
				soupnode.statics.haveDefaultNameSpace = true
			}
			soupnode.attributesURLHandler(soupnode)
		}
	}
	soupnode.writeHandler = defaultSettings.writeHandler
	soupnode.voiceHandler = defaultSettings.voiceHandler
	if isForcedToSpan == true {
		var nattr nodeAttribute
		nattr.name = data.AWWWEAttributeTagName
		nattr.prefix = data.EmptyString
		nattr.value = data.BytesToUTF8String(name)
		soupnode.attributes.append(&nattr)
		var pattr nodeAttribute
		pattr.name = data.AWWWEAttributeTagPrefix
		pattr.prefix = data.EmptyString
		pattr.value = soupnode.prefix
		soupnode.attributes.append(&pattr)
	}
	// at this point soupnode is or is not valid
	// return the current parent in the tree
	newParent = soupnode.currentParent(parent)
	return
}

// Closes the last html tag with a matching prefix and name.
// The returned *SoupNode is the current parent in the tree.
func CloseTagNode(parent *SoupNode, prefix, name []byte) *SoupNode {
	p := parent
	prefixString := data.BytesToUTF8String(prefix)
	nameString := data.BytesToUTF8String(name)
	for {
		if p == nil {
			// did not find a match for the end tag
			return parent
		}
		child := findLastOpenChild(p, prefixString, nameString, -1)
		if child != nil {
			// found a match for the end tab
			child.close()
			return child.parent
		} else {
			// maybe end tag is for parnet not for a child
			p = p.parent
		}
	}
}

// Adds a new text node to the tree.
// The returned *SoupNode is the current parent in the tree.
func AddNewTextNode(parent *SoupNode, value []byte) (soupnode, newParent *SoupNode) {
	soupnode = newSoupNode()
	soupnode.nodeType = nodeTypeText
	soupnode.value = data.BytesToUTF8String(value)
	soupnode.isClosed = true
	soupnode.isValid = true
	soupnode.isOutput = true
	soupnode.isVisible = true
	validParent := soupnode.findValidParent(parent)
	if validParent != nil {
		soupnode.parent = validParent
		validParent.children.Append(soupnode)
	} else {
		soupnode.isValid = false
	}
	soupnode.writeHandler = writeToBufferText
	soupnode.voiceHandler = writeToBufferValueVoice
	newParent = soupnode.currentParent(parent)
	return
}

func (this *SoupNode) currentParent(parent *SoupNode) *SoupNode {
	// this soupnode is or is not valid
	// return what should be the current parent in the tree
	if this.isValid == true {
		// valid child
		if this.isClosed == true {
			// tag is closed, no more children
			// continue with parent (not necessarily the child's parent)
			// because who knows where this child ended up.
			return parent
		} else {
			// open tag
			// continue with this child as the new parent
			return this
		}
	} else {
		// invalid child
		// ignore this tag, continue with parent
		return parent
	}
}

func (this *SoupNode) findValidParent(p *SoupNode) *SoupNode {
	for {
		if this.isValidParent(p) {
			return p
		} else {
			p = p.parent
		}
	}
}

func (this *SoupNode) isValidParent(p *SoupNode) bool {
	if p == nil {
		return true
	}
	if this.nodeType == nodeTypeText {
		if p.nodeType == nodeTypeDocument || p.name == "html" || p.name == "head" {
			return false
		} else {
			return true
		}
	} else {
		if this.validParentNames.Len() > 0 {
			// this parent must be valid
			return this.validParentNames.Contains(p.name)
		}
		if this.inValidParentNames.Len() > 0 {
			// this parent must not be invalid
			return !this.inValidParentNames.Contains(p.name)
		}
		if p.validChildrenNames.Len() > 0 {
			return p.validChildrenNames.Contains(this.name)
		}
		if this.isBlock == true {
			// block can only have a block parent or inline block parent
			return p.isBlock == true || p.canBeBlock == true
		} else if this.isHeadDecendent == true {
			// must decend from the head tag
			for {
				if p.name == "head" {
					return true
				}
				p = p.parent
				if p == nil {
					return false
				}
			}
		} else if this.isBodyDecendent == true {
			// must decend from the body tag
			for {
				if p.name == "body" {
					return true
				}
				p = p.parent
				if p == nil {
					return false
				}
			}
		} else {
			// any parent is valid
			return true
		}
	}
}

func (this *SoupNode) initializeDocumentNameSpaces() {
	var attribute nodeAttribute
	node := this.parent
	if node != nil {
		// set the document name spaces
		this.statics = node.statics
		// set the tree node count
		// set the node counts and node id
		for {
			node.nodeCount++
			if node.parent == nil {
				// root node
				if this.id == -1 {
					this.id = node.nodeCount
					// set the AWWWEAttributeId
					attribute.prefix = data.EmptyString
					attribute.name = data.AWWWEAttributeId
					attribute.value = fmt.Sprintf("%d", this.id)
					this.attributes.append(&attribute)
				}
				break
			} else {
				node = node.parent
			}
		}
		if len(this.prefix) == 0 {
			this.defaultNameSpace = this.parent.defaultNameSpace
		}

		// set is valid using hidden attribute
		if this.isValid == true {
			this.isValid = !this.attributes.containsName("hidden")
		}
	}
}

func findLastOpenChild(parent *SoupNode, prefix, name string, id int) (child *SoupNode) {
	p := parent
	for {
		if p == nil {
			child = nil
			return
		}
		// todo: must be open child
		for i := p.children.Len() - 1; i >= 0; i-- {
			child = p.children.AtIndex(i)
			if child.prefix == prefix && child.name == name &&
				child.id != id && child.isClosed == false {
				return
			}
		}
		// maybe end tag is for parnet not for a child
		p = p.parent
	}
}

func (soupnode *SoupNode) close() {
	if soupnode.isClosed == false {
		// close the children from last to first
		for i := soupnode.children.Len() - 1; i >= 0; i-- {
			child := soupnode.children.AtIndex(i)
			child.close()
		}
		// now this node is closed
		soupnode.isClosed = true
	}
}
